var map;
// The selected velocity field:
var uvindex = 0;
// keep track of changes to the form (via events) and map, to see if we need to resubmit or not:
var formIsDirty = false;
// keep the form validator for validating individual elements:
var validator;

// If the extent is be bigger than this we will refuse submission
// (assuming that they unintentionally have another region of the map
// selected from a previous run).
var maxDimension = 2;

// These should match, but moment.js and jquery-datepicker use different formats strings:
var dateFormat = 'd/m', momentFormat = 'D/M';

function loginSubmit() {
  // note the use of a bit of jquery navigation to extract the
  // submission url, rather than hard-coding it:
  $.post($('#loginform').attr('action'), {
           'email':$('#id_email').val(),
           'password':$('#id_password').val(),
           'next':$('#loginnext').val()
         },
         function(data) {
           $('#facebox .content').empty().html(data);
           onAuthSubmit(data);
         });
  return false;
}

function logoutSubmit() {
  $.post($(this).attr('href'),
         {},
         onLogoutSuccess);
  return false;
}

function onLogoutSuccess(data) {
  $('#loginplaceholder').show('fast');
  $('#logoutplaceholder').hide('fast');
}

function registrationSubmit() {
  // note the use of a bit of jquery navigation to extract the
  // submission url, rather than hard-coding it:
  $.post($('#registrationform').attr('action'), {
           'email':$('#id_email').val(),
           'password1':$('#id_password1').val(),
           'password2':$('#id_password1').val()
         },
         function(data) {
           $('#facebox .content').empty().html(data);
           onRegistrationSuccess(data);
         });
  return false;
}

// Null function called on successful registration (note; this used to
// login the user, but since we use a delayed registration where they
// must confirm via email we don't do anything right now).
function onRegistrationSuccess(data) {
}

// Called with the data received from a login request.  Note that actual session
// login is still performed on the server; this just alters details on
// the page.
function onAuthSubmit(data) {
  var username = $(data).find('#username').text();
  if (username){
    doLoginUser(username);
  }
}

function doLoginUser(username) {
  $('.username').text(username);
  $('#logoutplaceholder').show('fast');
  $('#loginplaceholder').hide('fast');
}

// returns the GET parameters used for the main results request; the '?' is not prepended.
function formatGetParameters() {
  return 'cmap=' + $('#cbar :selected').val() +
    '&maxrange=' + $('#cbarmax :selected').val();
}

// will only have any effect if the form isn't dirty:
function updateResultsUrl() {
  if (!formIsDirty) {
    var url = $('#finalstats').attr('checked') ?
      $('#paraminputs').data('finalploturl')
      : $('#paraminputs').data('allploturl');
    map.setResultsURL(url  + '?' + formatGetParameters());
  }
}


function formatDate(str) {
  // want dd/mm/yyyy; json data is in yyyy-mm-dd:
  return str.split('-').reverse().join('/');
}
var onUVSelect = function(idx) {
  uvindex = idx;
  var uvfield = fieldinfo[uvindex];
  // refresh year list:
  var yearFromDateStr = function(str) {
    return parseInt(str.slice(0,4), 10);
  };
  var ymin = yearFromDateStr(uvfield.minDate), ymax = yearFromDateStr(uvfield.maxDate);
  var yearlist = $('#yearselection');
  yearlist.empty();
  for (var y = ymin; y <= ymax; y++) {
    yearlist.append($('<option></option>').val(yearinfo[y]).html(y));
  }
  // refresh vertical levels lists:
  var levelslist = $('select[name$=-verticalLevel]');
  levelslist.empty();
  for (var i = 0; i < uvfield.depths.length; i++) {
    var pk = uvfield.depths[i].pk;
    var depth = uvfield.depths[i].fields.depth;
    var ltxt = depth == 0 ? 'floating' : depth + 'm';
    levelslist.append($('<option></option>').val(pk).html(ltxt));
  }
  // refresh the available date range:
  $('#mindate').text(formatDate(uvfield.minDate));
  $('#maxdate').text(formatDate(uvfield.maxDate));
  // change the selection bounds as well:
  // setBoundsByIndex(selectionBoundsArray[idx]);
  // Refresh the map's representation:
  if (map) {
    map.setUVFields(uvfield);
    // lastly, clear the map selections as well as they could be outside the available bounds (may want to think about this later, prune/warn etc)
    map.clear();
  }
};

function getDispersalTime() {
  var dispersalTime = 0;
  $('#phaseTabs').find('[name$="-phaseLongevity"]').each(function(i, el) {
    dispersalTime += parseInt($(el).val(), 10);
  });
  return dispersalTime;
};

// If there's a #runid fragment in the URL, try and load that data and
// kick off a run:
function attemptConfigLoad(){
  var hash = window.location.hash, runid = 0;
  if (hash && (runid = +hash.substring(1))) {
    $.event.trigger('conniestarted');
    $.ajax({
      url: 'configurations/' + runid + '/',
      dataType: 'json',
      success: function(conf, status) {
        var form = conf.form, results = conf.results, urls = conf.urls;

        // Populate the form, just so users get the initial values
        for (var id in form) {
          // The keys id & isSource should be safely ignored
          $('#' + id).val(form[id]);
        }
        var fromdate = moment(form.id_spawningFromDate),
            todate = moment(form.id_spawningToDate);
        $('#fromdate').val(fromdate.format(momentFormat));
        $('#todate').val(todate.format(momentFormat));
        $(form['isSource'] ? '#source' : '#sink').attr('checked', 'checked');
        map.setUserRegions(form.id_regions);

        // Store URLs where they're expected:
        var $params = $('#paraminputs');
        for (var u in urls) {
          $params.data(u, urls[u]);
        }
        // Kick off the processing:
        map.newResultsLayer(urls.allploturl + '?' + formatGetParameters(),
                            results.width, results.height,
                            results.x0, results.y0,
                            results.x1, results.y1,
                            results.id);
      },
      error: function() {
        // If we get an error, try to gently inform them and let them
        // continue (reset the hash-fragment, for consistency):
        var dudid = window.location.hash.substring(1);
        window.location.hash = '';
        $.event.trigger('connieerror', [null, "We couldn't find that record (" +
                                        dudid + "), sorry; please feel free " +
                                        "to explore Connie on your own!"]);
      }
    });
  }
}

window.$ = window.jQuery;
$(function() {
  // Process the UV-fields info array:
  var wktConverter = new OpenLayers.Format.WKT(); // temporary instance
  // pre-process to remove the serialisation artifacts (primary key, etc):
  fieldinfo = $.map(fieldinfo, function(obj,i) {
    var field = obj.fields;
    // As a functional programmer I'm not happy about the side effect
    // in a map statement, but it's more efficient than looping twice:
    $('#uvfieldslist').append(
      $('<option></option>').val(obj.pk).html(field.name));
    field.bounds = wktConverter.read(field.bounds);
    field.bounds.data.id = obj.pk;
    field.bounds.data.uvindex = i;
    return field;
  });

  // Slightly hackish: the issue is that with the form validation we
  // need to make sure that validation /doesn't/ happen on blur, as
  // one of the custom validation methods calling getDate invokes
  // this, leading to the date widget mis-behaving (I think this is
  // what is happening anyway; not 100% sure).  So, we set keyup to
  // trigger validation instead, but this means that we need to
  // generate a keyup event on the year selection list (see also, the
  // datepickers themselves and onclose):
  $('#yearselection').change(function() {
    $(this).trigger('keyup');
  });

  // datepickers initialisation:
  $('#fromdate').datepicker({
      dateFormat:dateFormat,
      altField:'#id_spawningFromDate',
      altFormat:'yy-mm-dd',
      // see above comment about generating a keyup event for the
      // #yearselection list
      onClose: function(datetxt,inst) {
        $(this).trigger('keyup');
      }
  });
  $('#todate').datepicker({
      dateFormat:dateFormat,
      altField:'#id_spawningToDate',
      altFormat:'yy-mm-dd',
      // see above comment about generating a keyup event for the
      // #yearselection list
      onClose: function(datetxt,inst) {
        $(this).trigger('keyup');
      }
  });
  $.validator.addMethod(
    "noyeardate",
    function(value, element) {
      return new RegExp('^\\s*\\d{1,2}/\\d{1,2}\\s*$').test(value);
    },
    "Please use the format " + dateFormat);
  $.validator.addMethod(
    "validnoyeardate",
    function(value, element) {
      // check against the date format string, making sure we specify a non leap-year:
      return moment(value + '/1999', momentFormat + '/YYYY').isValid();
    },
    "Must be a valid date (in the format " + dateFormat + ")");
  $.validator.addMethod("enddataavailable",
    // Figure out if the last day selected (date plus max selected year) is less than
    // the last day of available data.
    function(value, element) {
      var lastSelectedYear = $.map($('#yearselection :selected'), function(e) { return $(e).text(); }).reverse()[0];
      // ignore if they haven't selected a year yet:
      if (!lastSelectedYear)
        return true;
      var issource = $('#source').attr('checked');
      // Construct max available date from the string in fields info:
      var lastAvailableDay = moment(fieldinfo[uvindex].maxDate).hours(23).minutes(59);
      // construct last day of simulation by adding the dispersal time onto the last release date:
      var spawningToDate = moment($('#todate').datepicker('getDate'))
          .hours(23).minutes(59).year(lastSelectedYear);
      var lastDayOfSimulation = spawningToDate.add('days', issource ? getDispersalTime() : 0);
      return lastDayOfSimulation.diff(lastAvailableDay) <= 0;
    },
    function () {
     return $('#source').attr('checked') ?
      "The dispersal time, plus the latest release date, extends past the available data."
      : "The dispersal time extends past the available data.";
    });
  $.validator.addMethod("begindataavailable",
    function(value, element) {
      var firstSelectedYear = $.map($('#yearselection :selected'), function(e) { return $(e).text(); })[0];
      // ignore if they haven't selected a year yet:
      if (!firstSelectedYear)
        return true;
      var issink = $('#sink').attr('checked');
      // Construct first day of available data:
      var firstAvailableDay = moment(fieldinfo[uvindex].minDate).hours(0).minutes(0);
      // construct last day of simulation by adding the dispersal time onto the last release date:
      var spawningFromDate = moment($('#fromdate').datepicker('getDate'))
          .hours(0).minutes(0).year(firstSelectedYear);
      var minimumSimulationDay = spawningFromDate.subtract('days', issink ? getDispersalTime() : 0);
      return minimumSimulationDay.diff(firstAvailableDay) >= 0;
    },
    function () {
      return $('#source').attr('checked') ?
        "The start date is before the beginning of the available data."
        : "The start date (less the dispersal time) is before the beginning of the available data.";
    });
  $.validator.addMethod("percentage",
    function(value, element) {
      var pctg;
      if (/^[0-9]+$/.test(value) && !isNaN(pctg = parseInt(value, 10))) {
        return 0 <= pctg && pctg <= 100;
      }
      return false;
    },
    "Wind speed must be an integral percentage (between 0 and 100) ");

  // client-side form validation and submission:
  validator = $('#paraminputs').validate({
    focusInvalid: false,
    // Subtle point here: validate on keyup (default), but /not/ the
    // default of blur, as this interfers with the date widget (which
    // forces a focus change, as far as I can work out):
    onfocusout: false,
    submitHandler: function(form) {
      // Ugly, but there's an issue where opening the dialogue seems
      // to trigger a focusout even which then tries to submit the form,
      // so guard against that:
      if ($('#behavioursTarget').dialog('isOpen')) {
        return false;
      }

      var $form = $(form);
      // just in case:
      $('#behavioursTrigger').removeClass('error');
      // behaviours has some fields disabled occasionally, but for validation
      // reasons they must still be submitted.  Re-enable them so that happens:
      $form.find(':input').removeAttr('disabled');

      var bounds = map.userSelectionExtent();
      // check that they haven't asked for too-large a region:
      if (bounds.getWidth() > maxDimension ||
               bounds.getHeight() > maxDimension) {
        $.facebox('<span id="errormessages">For performance reasons we are ' +
                  'limiting the size of the area that can be submitted; ' +
                  'please remove some regions and try again (do you have ' +
                  'regions from previous simulations still active, perhaps?)</span>');
      }
      // if nothing that affects the plot (other than colour and stats-type) has changed, simply update the results url:
      else if (!formIsDirty) {
        updateResultsUrl();
      }
      // otherwise, do a full form submission:
      else {
        $.event.trigger('conniestarted');
        $.ajax({
                 url: $('#paraminputs').attr('action'),
                 type: 'POST',
                 data: $form.serialize(),
                 //timeout: 2000,
                 success: function(data) {
                   // data is a html fragment containing the url of
                   // the results plot and the min and max bounds (the
                   // extent) of the image.

                   // extract results:
                   var results = $(data);
                   var finalploturl = results.find('#finalploturl').text(),
                     allploturl = results.find('#allploturl').text(),
                     finaldataurl = results.find('#finaldataurl').text(),
                     alldataurl = results.find('#alldataurl').text(),
                     finalkmlurl = results.find('#finalkmlurl').text(),
                     allkmlurl = results.find('#allkmlurl').text();
                   // store urls for later access:
                   $('#paraminputs').data('finalploturl', finalploturl);
                   $('#paraminputs').data('allploturl', allploturl);
                   $('#paraminputs').data('finaldataurl', finaldataurl);
                   $('#paraminputs').data('alldataurl', alldataurl);
                   $('#paraminputs').data('finalkmlurl', finalkmlurl);
                   $('#paraminputs').data('allkmlurl', allkmlurl);
                   var url = $('#finalstats').attr('checked') ?
                     finalploturl
                     : allploturl;

                   // for error reporting, get the run id as well:
                   var runid = results.find('#runid').text();
                   window.location.hash = runid;

                   var width  = parseInt(results.find('#imgwidth'),10),
                       height = parseInt(results.find('#imgheight'),10);

                   var x0=parseFloat(results.find('#boundsminx').text()),
                     y0=parseFloat(results.find('#boundsminy').text()),
                     x1=parseFloat(results.find('#boundsmaxx').text()),
                     y1=parseFloat(results.find('#boundsmaxy').text());
                   map.newResultsLayer(url + '?' + formatGetParameters(),
                                       width, height,
                                       x0, y0, x1, y1,
                                       runid);
                   // mark form as clean again:
                   formIsDirty = false;
                 },
                 error: function(xhr,status,error) {
                   $.event.trigger('connieerror', [null, 'There was an error creating the new configuration record, sorry.']);
                 }
               });
      }
      return false;
    },
    invalidHandler: function(form, validator) {
      // If any invalid elements are in the behaviours pane (and hence
      // not normally displayed), add an error class to the button as
      // well.
      if(validator.invalidElements().parents('#behavioursTabPane').length > 0) {
        $('#behavioursTrigger').addClass('error');
      }
      else {
        $('#behavioursTrigger').removeClass('error');
      }
    },
    showErrors: function(errorMap, errorList) {
      $('#errorheader').html("Please correct the following error" +
                             (this.numberOfInvalids() > 1 ? "s" : "") +
                             ":");
      this.defaultShowErrors();
    },
    messages : {
      'larvalPhases-0-phaseLongevity': {
        digits: "The dispersal time must be an integer",
        required: "The dispersal time is required"
      },
      fromdatevalidation: {
        required: "The start date must be specified (in format " + dateFormat + ")",
        noyeardate: "The start date must be in the format " + dateFormat,
        validnoyeardate: "The start date must be a valid date (in the format " + dateFormat + ")"
      },
      todatevalidation: {
        required: "The end date must be specified (in format " + dateFormat + ")",
        noyeardate: "The end date must be in the format " + dateFormat,
        validnoyeardate: "The end date must be a valid date (in the format " + dateFormat + ")"
      },
      regions: {
        required: "Please select at least one point on the map."
      },
      years: {
        required: "At least one year must be selected"
      }
    },
    errorContainer: "#errormessages",
    errorLabelContainer: "#errorlist",
    wrapper: "li"
  });
  // this doesn't seem to hide by default at first, for some reason; hack:
  $('#errormessages').hide();

  // propagate events when form details change:
  $('#paraminputs :input[class!=notProcessed]').live('change', function() {
    $.event.trigger('detailsChanged');
  });
  // catch events and update:
  $(document).bind('detailsChanged', function() {
    formIsDirty = true;
    // ideally we'd probably only do this when the map is changed, but
    // it doesn't really matter: basically, we want to make sure that
    // the selection layer is visible when the form is being edited,
    // eg if they click on the map when the results layer is visible
    // (and hence the selection layer ordinarily wouldn't be):
    map.toggleSelectionVisibility(true);
  });

  // use events to manage the download link:
  $('#downloadlink').bind('conniefinished', function() {
    var $location = $(this).find('ul');
    var setLink = function(txt, url, cls) {
      var $parent = $location.find('li.' + cls);
      var link = $parent.find('a');
      if (!link.length) {
        link = $('<a target="_blank"></a>');
        $parent.append(link);
      }
      link.attr('href', url).text(txt);
    };
    var imgurl, dataurl, kmlurl;
    if ($('#finalstats').attr('checked')) {
      imgurl  = $('#paraminputs').data('finalploturl');
      dataurl = $('#paraminputs').data('finaldataurl');
      kmlurl = $('#paraminputs').data('finalkmlurl');
    }
    else {
      imgurl  = $('#paraminputs').data('allploturl');
      dataurl = $('#paraminputs').data('alldataurl');
      kmlurl = $('#paraminputs').data('allkmlurl');
    }
    imgurl = imgurl + '?' + formatGetParameters() + '&standalone=true';
    kmlurl = kmlurl + '?' + formatGetParameters();
    setLink('Download this results image', imgurl, 'resimage');
    setLink('Download this results data', dataurl, 'resdata');
    setLink('Download results for Google Earth', kmlurl, 'reskml');
  });
  $('#downloadlink').bind('detailsChanged', function() {
    var $parent = $(this);
    $parent.find('li.resimage>a').text('Download last results image');
    $parent.find('li.resdata>a').text('Download last results data');
    $parent.find('li.reskml>a').text('Download last results for Google Earth');
  });

  // connie loading events; we'll throw a div with a loading-gif over the whole page
  // while processing is happening:
  $('body').bind('conniestarted', function() {
    $('body').elapsor({image: connie_media + 'processing.gif',
                       text: 'Processing; please wait...'});
    // make sure the selection layer is visible while they wait:
    map.toggleSelectionVisibility(true);
  });
  $('body').bind('conniefinished', function() {
    $('body').hideElapsor();
    // hide the selection layer:
      map.toggleSelectionVisibility(false);
  });
  $('body').bind('connieerror', function(evt, runid, msg) {
    $('body').hideElapsor();
    if (runid) {
      $.facebox('There was an error processing the results, sorry.  ' +
                'If you would like to help the development of Connie,' +
                ' please email the connie team and tell them what' +
                ' you were doing, mentioning the simulation number ' +
                runid + '.');
    }
    else {
      $.facebox(msg);
    }
  });

  // quick-links, logout (top-right corner) setup:
  $('a[rel=facebox]').facebox({
      opacity: 0.65,
      loadingImage: connie_media + 'img/loading.gif',
      closeImage: connie_media + 'img/closelabel.png'
  });
  $('.logout').click(logoutSubmit);

  // make sure we're using the same date format everywhere:
  $('.dateFormat').text(dateFormat);

  // clear-map button:
  $('#resetmap').click(function() { map.clear(); });

  // set up tooltips:
  $('.hastooltip').tooltip({
    track: true,
    showBody: "|"
  });

  function updateElementAttrs(el, regexp, replacement) {
    var id_regex = new RegExp(regexp);
    if ($(el).attr("for")) $(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
    if (el.id) el.id = el.id.replace(id_regex, replacement);
    if (el.name) el.name = el.name.replace(id_regex, replacement);
  }

  // This is a bit large, but to avoid polluting the namespace we do
  // all the work inside an anonymous function.  This is all to do
  // with the form-grid:
  (function() {
    // populate table templates from existing table contents, for some robustness:
    var headerTemplate, cellTemplate;
    // NOTE: this is explicitly to conform to python2.6; versions
    // earlier than that return a result host-word sized, and python3
    // is unsigned 32 bit -- python 2.6 should be signed 32 bit,
    // regardless of host platform.
    function crc32 (str) {
       var crc32tab = [
         0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
         0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
         0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
         0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
         0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
         0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
         0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
         0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
         0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
         0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
         0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
         0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
         0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
         0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
         0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
         0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
         0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
         0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
         0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
         0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
         0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
         0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
         0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
         0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
         0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
         0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
         0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
         0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
         0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
         0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
         0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
         0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
         0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
         0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
         0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
         0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
         0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
         0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
         0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
         0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
         0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
         0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
         0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
         0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
         0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
         0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
         0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
         0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
         0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
         0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
         0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
         0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
         0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
         0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
         0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
         0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
         0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
         0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
         0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
         0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
         0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
         0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
         0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
         0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d];
      var crc = ~0;
      for (var i = 0; i < str.length; i++)
        crc = (crc >>> 8) ^ crc32tab[(crc ^ str.charCodeAt(i)) & 0xff];
      // Not sure why, but this is needed to conform to python2.6 zlib.crc32() (I presume, in order to convert to signed 32bit)
      return ~crc;
    }
    function templateInit() {
      // note how we clone the cell elements, but then use their .children() as the template -- this is so .find() works:
      var $t = $('table:first');
      // clone and template-ise the first header cell:
      headerTemplate = $t.find('tr:eq(0)').find('th:eq(0)').clone();
      headerTemplate.find(':input,label').each(function() {
        updateElementAttrs(this, 'larvalPhases-\\d+', 'larvalPhases-INDEX');
        updateElementAttrs(this, 'LEVEL_-?\\d+', 'LEVEL_PARENTHASH');
      });
      headerTemplate = headerTemplate.children();
      // clone and template-ise the first non-header cell:
      cellTemplate = $t.find('tr:eq(1)').find('td:eq(0)').clone();
      cellTemplate.find(':input,label').each(function() {
        updateElementAttrs(this, 'LEVEL_-?\\d+-\\d+', 'LEVEL_PARENTHASH-INDEX');
      });
      cellTemplate = cellTemplate.children();
    }
    function addToolTips($el) {
      // This is a bit ugly, but until we get the equivalent of live() for arbitrary actions it'll do:
      var opts = {
        track: true,
        showBody: "|",
        showURL: false
      };
      $el.find('.cfgAddCol').attr('title', '|Add another phase').tooltip(opts);
      $el.find('.cfgColumnRemove').attr('title', '|Remove this phase').tooltip(opts);
      $el.find('.cfgAddCell').attr('title', '|Add another vertical level'). tooltip(opts);
      $el.find('.cfgRemoveCell').attr('title', '|Remove this level').tooltip(opts);
      $el.find('[title]').tooltip(opts);
    }
    function updatePhaseHeaders() {
      $('#behavioursGrid').find('.phasenumber').each(function(index) {
        $(this).text(index+1);
      });
    }
    function resizeDialog() {
      // Note, we only set the width here since the height shouldn't change
      // (also, because there's a glitch in there that I could track down, so
      // removing it to be on the safe side)
      var mainPanel = $('#behavioursTarget'), // Outer div associated with the jqui-dialog
          grid = $('#behavioursGrid'),        // The table, where the main dimensions are calculated
          content = $('#behavioursContent');  // Wrapper; used to calculate padding.
      var width =  grid.outerWidth(true) +
                     content.outerWidth(true) - content.outerWidth() +
                     mainPanel.outerWidth(true) - mainPanel.outerWidth(); // padding
      mainPanel.dialog('option', 'width', width);
    }
    function onColumnAdded(headerCell, dataCells) {
      // figure out index, use that; calculate hash and construct management form for levels; patch up first level entry; increment total number of forms
      var colidx = headerCell.parents('tr:first').children('th').index(headerCell);
      var prefix = 'LEVEL_' + crc32('larvalPhases-'+colidx);
      // adjust header ids:
      headerCell.find(':input,label').each(function() {
        updateElementAttrs(this, '(larvalPhases-INDEX)', 'larvalPhases-'+colidx);
        updateElementAttrs(this, '(LEVEL_PARENTHASH)', prefix);
      });
      // same for cell ids:
      dataCells.find('li').each(function(index) {
        $(this).find(':input,label').each(function() {
          updateElementAttrs(this, '(LEVEL_PARENTHASH-INDEX)', prefix+'-'+index);
        });
      });
      // Get the levels selector list from the first column (will always be
      // up to date), clone it and stick in the new level lists:
      dataCells.each(function(index) {
        var levels = $(this).parent('tr').find('td:first select[name$=verticalLevel]:first').children();
        $(this).find('select[name$=verticalLevel]').each(function() {
          $(this).empty().append(levels.clone());
        });
      });
      // increment total forms:
      $('#id_larvalPhases-TOTAL_FORMS').val(colidx+1);
      // patch up headings:
      updatePhaseHeaders();
      addToolTips(headerCell); addToolTips(dataCells);
      resizeDialog();
    }
    function onColumnRemoving(headerCell, dataCells) {
      // needs to process all following columns and adjust ids etc; decrement total number of forms
      var baseColIdx = headerCell.parents('tr:first').children('th').index(headerCell);
      // shuffle header ids and references of following headers:
      headerCell.nextAll('th').each(function(index) {
        var colidx = baseColIdx+index;
        var prefix = 'LEVEL_' + crc32('larvalPhases-'+colidx);
        $(this).find(':input,label').each(function() {
          updateElementAttrs(this, '(larvalPhases-\\d+)', 'larvalPhases-'+colidx);
          updateElementAttrs(this, '(LEVEL_-?\\d+)', prefix);
        });
        // patch up headings:
        $(this).find('.phasenumber').text(colidx+1);
      });
      // shuffle cell ids and references of following cells:
      dataCells.each(function(rowidx) {
        $(this).nextAll('td').each(function(idx) {
          var colidx = baseColIdx+idx;
          var prefix = 'LEVEL_' + crc32('larvalPhases-'+colidx) + '-';
          $(this).find(':input,label').each(function() {
            updateElementAttrs(this, 'LEVEL_-?\\d+-\\d+', prefix+rowidx);
          });
        });
      });
      // decrement total forms:
      $('#id_larvalPhases-TOTAL_FORMS').val(headerCell.siblings('th').length-1);
    }

    // The dialogue isn't part of the form when it is being edited (open), so
    // it won't trigger the same events that are used to detect if the form is
    // dirty or not.  Catch those separately.
    $('#behavioursContent :input[class!=notProcessed]').live('change', function() {
      $.event.trigger('detailsChanged');
    });

    function onClosing() {
      // Move the form contents from the dialog (outside the form) back into the form:
      $('#behavioursTabPane').append( $('#behavioursContent') );

      // Ich, shouldn't really be here... (see validation, above):
      var allValid = true;
      $('#behavioursContent').find(':input').each(function() {
        allValid = allValid && validator.element($(this));
      });
      if (allValid) {
        $('#behavioursTrigger').removeClass('error');
      }
      else {
        $('#behavioursTrigger').addClass('error');
      }

      var $headerRow = $('#behavioursGrid').find('tr:first');
      // calculate and update phase numbers:
      $headerRow.children('th').each(function(index) {
        $('#id_larvalPhases-' + index + '-phaseNumber').val(index+1);
      });
      // Print a summary of the number of phases configured:
      var colcount = $headerRow.children('th').length - 1; // assuming one empty col for the 'add' button
      var summary = colcount + ' phase' + (colcount>1?'s':'') + ' configured.';
      $('#behavioursSummaryText').text(summary);
    }
    onUVSelect(0);
    templateInit();
    var $grid = $('#behavioursGrid').connieFormGrid({
      headerContents: headerTemplate,
      maxRows: 1,
      newCellContents: cellTemplate,
      onColumnAdded: onColumnAdded,
      onColumnRemoving: onColumnRemoving,
      onColumnRemoved: resizeDialog
    });
    addToolTips($grid);
    $('#behavioursTarget').dialog({
      autoOpen: false,
      close: onClosing,
      modal: true,
      open: function() {
        // shift the form contents into the dialogue div so it can be displayed:
        $('#behavioursTarget').append( $('#behavioursContent') );
      },
      resizable: false,
      width: 'auto',
      height: 'auto',
      title: 'Behaviours Configuration',
      zIndex: 2000
    });
  })();
  $('#behavioursTrigger').click(function() {
    $('#behavioursTarget').dialog('open');
  });

  // Set up tabs for behaviours:
  // utility to swap out the tabs so only the selected one is contained in the
  // <form>.  This is invoked by a wrapper on tab change, which also revalidates
  // the form -- we do it this way so we can swap out on page load, without
  // instantly popping up "field is required" messages.
  var swapOutTabs = function(ui) {
    // not all elements are contained within a tab panel, so use the index to identify the parent:
    var tabids = {
      0: {
        tab: '#passiveContents',
        parent: '#passiveTab'
      },
      1: {
        tab: '#behavioursContent',
        parent: '#behavioursTabPane'
      }
    };
    var selectedidx = ui.index, otheridx = (ui.index+1) % 2;
    // swap the respective contents between their form parents and the holding div:
    $(tabids[selectedidx].parent).append( $(tabids[selectedidx].tab) );
    $('#nonformholder').append( $(tabids[otheridx].tab) );
    // enable the selected tab's form elements:
    $(tabids[selectedidx].tab).find(':input,label').each(function() {
      updateElementAttrs(this, 'disabled_', '');
    });
    // disable the other tab's form elements:
    $(tabids[otheridx].tab).find(':input,label').each(function() {
      updateElementAttrs(this, '(.*)', 'disabled_$1');
    });
  };
  // select the first one initially:
  swapOutTabs({ index: 0 });
  $('#phaseTabs').tabs({
    select: function(event, ui) {
      swapOutTabs(ui);
      // revalidate the form (to get rid of messages from the other tab):
      validator.form();
      // Trigger a change to mark the form as dirty; this means if they swap
      // and swap back it will still be dirty, but it's probably good enough:
      $.event.trigger('detailsChanged');
    }
  });

  // close the user-message, if displayed (first set the top correctly):
  $('#map').css('top', $('.usermessage').outerHeight(true));
  $('.usermessage > a.close').click(function (e) {
    e.preventDefault();
    $(this).parent().hide('fast', function () {
      $(this).remove();
      $('#map').css('top', 0);
      $(window).resize(); // Hack in case we're using js map sizing (IE7)
    });
    return false;
  });

  map = makeConnieMap('map', $('#id_regions'), fieldinfo[uvindex], colourBarURL);
  $('#uvfieldslist').change(function(){
    onUVSelect(this.selectedIndex);
  });

  // set up handlers for the colour-map selectors:
  var changeCbar = function(idx) {
    map.setColourSchemeByIndex(idx);
  };

  // For form elements that can be re-drawn from the cache, update as soon as they're changed:
  $('.notProcessed').change(function() {
    changeCbar($('#cbar').get(0).selectedIndex);
    updateResultsUrl();
  });
  changeCbar($('#cbar :selected').val()); // (sync on reload, in case it's not the default)

  // Now that we have a map, in case of IE7 (haven't been able to fix
  // with CSS alone unfortunately; I'm sure it's possible) make sure that
  // it resizes properly:
  if ($('html').hasClass('lt-ie8')) {
    var resizeMap = function() {
      var map = $('#map'),
          container = $('#mapcontainer'),
          w = container.width(),
          h = container.height(),
          top = parseInt($('#map').css('top'), 10);
      map.width(w).height(h - top - 10);
    };
    $(window).resize(resizeMap);
    resizeMap();
  }

  // Select default/only UV fields to kick things off:
  onUVSelect(0);
  attemptConfigLoad();

});
